package com.example.lotterysystem.service.mq;

import cn.hutool.core.date.DateUtil;
import com.example.lotterysystem.common.exception.ServiceException;
import com.example.lotterysystem.common.utils.JacksonUtil;
import com.example.lotterysystem.common.utils.MailUtil;
import com.example.lotterysystem.common.utils.SMSUtil;
import com.example.lotterysystem.controller.param.DrawPrizeParam;
import com.example.lotterysystem.dao.dataobject.ActivityPrizeDO;
import com.example.lotterysystem.dao.dataobject.WinningRecordDO;
import com.example.lotterysystem.dao.mapper.ActivityPrizeMapper;
import com.example.lotterysystem.dao.mapper.WinningRecordMapper;
import com.example.lotterysystem.service.DrawPrizeService;
import com.example.lotterysystem.service.activitystatus.ActivityStatusManager;
import com.example.lotterysystem.service.dto.ConvertActivityStatusDTO;
import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;
import com.example.lotterysystem.service.enums.ActivityPrizeTiersEnum;
import com.example.lotterysystem.service.enums.ActivityStatusEnum;
import com.example.lotterysystem.service.enums.ActivityUserStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.example.lotterysystem.common.config.DirectRabbitConfig.QUEUE_NAME;

@Component
@RabbitListener(queues = QUEUE_NAME)
public class MqReceiver {

    private static final Logger logger = LoggerFactory.getLogger(MqReceiver.class);

    @Autowired
    private DrawPrizeService drawPrizeService;

    @Autowired
    private ActivityStatusManager activityStatusManager;

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @Autowired
    private MailUtil mailUtil;

    @Autowired
    private SMSUtil smsUtil;

    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;

    @Autowired
    private WinningRecordMapper winningRecordMapper;

    @RabbitHandler
    public void process(Map<String,String> message){
        //mq成功接收到消息
        logger.info("mq成功接收到消息 message:{}",
                JacksonUtil.writeValueAsString(message));
        String paramString=message.get("messageData");
        DrawPrizeParam param=JacksonUtil.readValue(paramString,DrawPrizeParam.class);

        //处理抽奖流程

        //1.校验请求是否有效
        try {
            if (!drawPrizeService.checkDrawPrizeParam(param)){
                return;
            }
            //drawPrizeService.checkDrawPrizeParam(param);
            //2.状态扭转处理(非常重要)
            convertStatus(param);
            //3.保存中奖者名单
            List<WinningRecordDO> winningRecordDOList = drawPrizeService.saveWinnerRecords(param);
            //4.通知中奖者
            //抽奖之后的后续流程，异步（并发）处理
            syncExecute(winningRecordDOList);
        }catch (ServiceException e){
            logger.error("处理 MQ 消息异常，{}:{}",e.getCode(),e.getMessage(),e);
            //5.如果异常，需要保证事务的一致性，抛出异常,进行回滚
            //抛出异常:消息重试，(解决异常：代码bug，网络问题，服务问题)
            rollback(param);
            throw e;
        }catch (Exception e){
            logger.error("处理 MQ 消息异常，e:{}",e);
            rollback(param);
            throw e;
        }

    }

    /**
     * 处理抽奖异常的回滚行为
     * @param param
     */
    private void rollback(DrawPrizeParam param) {
        //1.回滚状态，活动，奖品，人员状态
        //判断是否需要回滚
        if (!statusNeedRollback(param)){
            //不需要：return
            return;
        }
        //需要：回滚状态
        statusRollback(param);

        //2.回滚中奖者名单
        //判断是否需要回滚
        if (!winnerNeedRollback(param)){
            //不需要：return
            return;
        }
        //需要：回滚中奖者名单
        winnerRollback(param);
    }

    /**
     * 回滚中奖者名单
     * @param param
     */
    private void winnerRollback(DrawPrizeParam param) {
        drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
    }

    /**
     * 判断中奖者名单是否需要回滚
     * @param param
     * @return
     */
    private boolean winnerNeedRollback(DrawPrizeParam param) {
        //判断活动中的奖品是否存在中奖者
        int count = winningRecordMapper.countByAPId(param.getActivityId(),param.getPrizeId());
        return count>0;
    }

    /**
     * 回滚状态
     * @param param
     */
    private void statusRollback(DrawPrizeParam param) {
        //涉及状态恢复，使用ActivityStatusManager
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT);
        convertActivityStatusDTO.setUserIds(
                param.getWinnerList()
                        .stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT);
        activityStatusManager.rollbackHandleEvent(convertActivityStatusDTO);
    }

    /**
     * 判断状态是否需要回滚
     * @param param
     * @return
     */
    private boolean statusNeedRollback(DrawPrizeParam param) {
        //查活动表，活动奖品表和活动人员表
        //但是在扭转状态时，我们保持了事务的一致性
        //所以，此时奖品和人员状态要么都扭转成功，要么都没扭转成功(不包含活动)
        ActivityPrizeDO activityPrizeDO=activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
        return activityPrizeDO.getStatus().equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
    }

    /**
     * 并发处理抽奖后续流程
     * 通知中奖者
     * @param winningRecordDOList
     */
    private void syncExecute(List<WinningRecordDO> winningRecordDOList) {
        //通过线程池来实现并发处理 threadPoolTaskExecutor

        //扩展：通过策略模式或者其他设计模式来完成后续的操作
        //短信通知
        threadPoolTaskExecutor.execute(()->sendMessage(winningRecordDOList));
        //邮箱通知
        threadPoolTaskExecutor.execute(()->sendMail(winningRecordDOList));
    }

    /**
     * 发邮件
     * @param winningRecordDOList
     */
    private void sendMail(List<WinningRecordDO> winningRecordDOList) {
        if (CollectionUtils.isEmpty(winningRecordDOList)){
            logger.info("中奖者列表为空，不需要发送邮件");
        }

        for (WinningRecordDO winningRecordDO:winningRecordDOList){
            String context ="Hi,"+ winningRecordDO.getWinnerName()+"。恭喜你在"
                    + winningRecordDO.getActivityName()+"活动中获得"
                    + ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage()
                    +":"+ winningRecordDO.getPrizeName()+"。获奖时间为"
                    + DateUtil.formatTime(winningRecordDO.getWinningTime())+"，请尽快领 取您的奖励!";
            mailUtil.sendSampleMail(winningRecordDO.getWinnerEmail(),"中奖通知",context);
        }

    }

    /**
     * 法短信
     * @param winningRecordDOList
     */
    private void sendMessage(List<WinningRecordDO> winningRecordDOList) {
        if (CollectionUtils.isEmpty(winningRecordDOList)){
            logger.info("中奖者列表为空，不需要发送短信");
        }

        for (WinningRecordDO winningRecordDO:winningRecordDOList){
            Map<String, String> map = new HashMap<>();
            map.put("name", winningRecordDO.getWinnerName());
            map.put("activityName", winningRecordDO.getActivityName());
            map.put("prizeTiers", ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage());
            map.put("prizeName", winningRecordDO.getPrizeName());
            map.put("winningTime", DateUtil.formatTime(winningRecordDO.getWinningTime()));
            smsUtil.sendMessage("SMS_465985911",
                    winningRecordDO.getWinnerPhoneNumber().getValue(),
                    JacksonUtil.writeValueAsString(map));
        }

    }

    /**
     * 状态扭转
     * @param param
     */
    private void convertStatus(DrawPrizeParam param){
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
        convertActivityStatusDTO.setUserIds(
                param.getWinnerList().stream()
                        .map(DrawPrizeParam.Winner::getUserId)
                        .collect(Collectors.toList())
        );
        convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.COMPLETED);
        activityStatusManager.handleEvent(convertActivityStatusDTO);
    }

    /*private void convertStatus(DrawPrizeParam param) {


        //问题：
        //1.活动状态的扭转依赖于活动奖品状态的扭转，导致代码维护性差---通过责任链设计模式来解决
        //2.状态扭转条件可能会扩展，当前写法，拓展性差，维护性差 --- 通过策略模式来解决

        // 活动  该活动中全部活动奖品的状态扭转后才能扭转
        // 活动奖品
        // 活动人员

        //1.扭转奖品状态
        //从数据库中查询活动奖品的信息
        //判断活动奖品状态是否不是COMPLETED，如果是，则不扭转，如果不是，则扭转状态
        //才扭转

        //2.扭转人员信息
        //从数据库中查询活动人员的信息
        //判断活动人员状态是否不是COMPLETED，如果是，则不扭转，如果不是，则扭转状态
        //才扭转

        //3.扭转活动转态
        //从数据库中查询活动的信息
        //判断活动状态是否不是COMPLETED，如果是，则不扭转，如果不是，则要等待活动奖品状态全部扭转完后，才去扭转活动状态
        //才扭转

        //4.更新缓存中存储的活动信息
    }*/

}
